/*
 * Copyright 2007 Google Inc.
 * Copyright 2007 The Apache Software Foundation.
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <apr_strings.h>

#include <ap_config.h>
#include <ap_provider.h>
#include <httpd.h>
#include <http_config.h>
#include <http_core.h>
#include <http_log.h>
#include <http_protocol.h>
#include <http_request.h>

#include <mod_auth.h>

/*
    This module implements a "filesystem-as-database" view of authn data.
    See gvn.userdb Python module for details.

    The configuration directive "AuthGvnUserDb" is used to set the
    directory underwhich all authn is to be found (herein referred to as
    "$dbdir").

    This is a sample set of configuration directives:
    AuthType basic
    AuthName svn_repos
    AuthBasicProvider gvn_userdb
    AuthGvnUserDb /opt/svn/userdb
    Require valid-user

    Regular htpasswd-type files are located at:
        $dbdir/somehash(<username>)/<username>/password

    The format of this file is:
        <crypted_password>[:<opaque...>]

    Regular digest-type files are located at:
        $dbdir/somehash(<username>)/<username>/realm_<realm>

    The format of this file is:
        <realm_hash>[:<opaque...>]

    "somehash()" is currently the first HASH_BYTES chars of the username,
    as in "mer" for "merlin"

    3 letters gives a total of 17576 possible directories, although in
    real life it'll be more around 5-10k due to the distribution of
    letters in logins. This is under the 32,000 directory limit for linux
    ext2 and ext3 filesystems, which is good.
    This hash level then gives us 100 to 500 files per directory for 1
    million users, which is fine. and still lets you scale up to around 10
    million users before you have to worry about big directories

    Note that you should still use a filesystem that has directory hashing.
    (dir_index option in ext3)

    This module was created by ek and merlin for the gvn project:
    http://code.google.com/p/gvn/
    where you will find admin-userdb and populate-userdb as well as
    lib/gvn/userdb.py to administer the on disk database.
 */

#define HASH_BYTES 3

module AP_MODULE_DECLARE_DATA authn_gvn_userdb_module;

typedef struct {
    char *userdb;
} authn_userdb_config_rec;

static void *create_authn_userdb_config(apr_pool_t *p, char *d)
{
    authn_userdb_config_rec *conf = apr_palloc(p, sizeof(*conf));

    conf->userdb = NULL;     /* just to illustrate the default really */
    return conf;
}

static const char *set_authn_userdb_slot(cmd_parms *cmd, void *offset,
                                      const char *f)
{
    return ap_set_file_slot(cmd, offset, f);
}

static const command_rec authn_userdb_cmds[] =
{
    AP_INIT_TAKE1("AuthGvnUserDb", set_authn_userdb_slot,
                  (void *)APR_OFFSETOF(authn_userdb_config_rec, userdb),
                  OR_AUTHCFG,
                  "directory of text files containing user passwords"),

    {NULL}
};

static apr_status_t open_per_user_authn_file(ap_configfile_t **f,
                                             request_rec *r,
                                             const char *user,
                                             const char *filename)
{
    int i;
    apr_status_t status;

    authn_userdb_config_rec *conf = ap_get_module_config(r->per_dir_config,
                                                         &authn_gvn_userdb_module);

    /* hash function is trivial, just take the first chars */
    char *hash = apr_pstrndup(r->pool, user, HASH_BYTES);

    char *user_file = apr_pstrcat(r->pool, conf->userdb, "/", hash,
                                  "/", user, "/", filename, NULL);

    status = ap_pcfg_openfile(f, r->pool, user_file);

    if (status != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
                      "Could not open user authn file: %s", user_file);
    }

    return status;
}

static char *get_first_word(apr_pool_t *p, ap_configfile_t *f,
                            const char delim)
{
    char * word = NULL;
    char l[MAX_STRING_LEN];

    while (!(ap_cfg_getline(l, MAX_STRING_LEN, f))) {
        const char *rpw;

        /* Skip # or blank lines. */
        if ((l[0] == '#') || (!l[0])) {
            continue;
        }

        rpw = l;

        word = ap_getword(p, &rpw, delim);
        break;
    }

    return word;
}

static authn_status check_password(request_rec *r, const char *user,
                                   const char *password)
{
    authn_userdb_config_rec *conf = ap_get_module_config(r->per_dir_config,
                                                         &authn_gvn_userdb_module);
    ap_configfile_t *f;
    apr_status_t status;
    char *file_password = NULL;

    /* Try to open "$dbdir/hash(<user>)/<user>/passwd" */
    status = open_per_user_authn_file(&f, r, user, "password");
    if (status != APR_SUCCESS) {
        if (APR_STATUS_IS_ENOENT(status)) {
            return AUTH_USER_NOT_FOUND;
        }
        return AUTH_GENERAL_ERROR;
    }

    file_password = get_first_word(r->pool, f, ':');
    ap_cfg_closefile(f);

    if (!file_password) {
        return AUTH_USER_NOT_FOUND;
    }

    status = apr_password_validate(password, file_password);
    if (status != APR_SUCCESS) {
        return AUTH_DENIED;
    }

    return AUTH_GRANTED;
}

static authn_status get_realm_hash(request_rec *r, const char *user,
                                   const char *realm, char **rethash)
{
    authn_userdb_config_rec *conf = ap_get_module_config(r->per_dir_config,
                                                         &authn_gvn_userdb_module);
    ap_configfile_t *f;
    apr_status_t status;
    char *file_hash = NULL;

    char *realm_file = apr_pstrcat(r->pool, "realm_", realm, NULL);

    /* Try to open "$dbdir/hash(<user>)/<user>/realm_<realm>" */
    status = open_per_user_authn_file(&f, r, user, realm_file);
    if (status != APR_SUCCESS) {
        if (APR_STATUS_IS_ENOENT(status)) {
            return AUTH_USER_NOT_FOUND;
        }
        return AUTH_GENERAL_ERROR;
    }

    file_hash = get_first_word(r->pool, f, ':');
    ap_cfg_closefile(f);

    if (!file_hash) {
        return AUTH_USER_NOT_FOUND;
    }

    *rethash = file_hash;

    return AUTH_USER_FOUND;
}

static const authn_provider authn_userdb_provider =
{
    &check_password,
    &get_realm_hash,
};

static void register_hooks(apr_pool_t *p)
{
    ap_register_provider(p, AUTHN_PROVIDER_GROUP, "gvn_userdb", "0",
                         &authn_userdb_provider);
}

module AP_MODULE_DECLARE_DATA authn_gvn_userdb_module =
{
    STANDARD20_MODULE_STUFF,
    create_authn_userdb_config,      /* dir config creater */
    NULL,                            /* dir merger --- default is to override */
    NULL,                            /* server config */
    NULL,                            /* merge server config */
    authn_userdb_cmds,               /* command apr_table_t */
    register_hooks                   /* register hooks */
};
